// Copyright 2024 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

// Package controller provides the server part of the *api.Client interface.
// nolint: dupl
package controller

import (
	"errors"
	"fmt"
	"net/http"

	"github.com/google/syzkaller/syz-cluster/pkg/api"
	"github.com/google/syzkaller/syz-cluster/pkg/app"
	"github.com/google/syzkaller/syz-cluster/pkg/service"
)

type APIServer struct {
	seriesService      *service.SeriesService
	sessionService     *service.SessionService
	buildService       *service.BuildService
	testService        *service.SessionTestService
	findingService     *service.FindingService
	baseFindingService *service.BaseFindingService
}

func NewAPIServer(env *app.AppEnvironment) *APIServer {
	return &APIServer{
		seriesService:      service.NewSeriesService(env),
		sessionService:     service.NewSessionService(env),
		buildService:       service.NewBuildService(env),
		testService:        service.NewSessionTestService(env),
		findingService:     service.NewFindingService(env),
		baseFindingService: service.NewBaseFindingService(env),
	}
}

func (c APIServer) Mux() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("/builds/last", c.getLastBuild)
	mux.HandleFunc("/builds/upload", c.uploadBuild)
	mux.HandleFunc("/findings/upload", c.uploadFinding)
	mux.HandleFunc("/series/upload", c.uploadSeries)
	mux.HandleFunc("/series/{series_id}", c.getSeries)
	mux.HandleFunc("/sessions/upload", c.uploadSession)
	mux.HandleFunc("/sessions/{session_id}/series", c.getSessionSeries)
	mux.HandleFunc("/sessions/{session_id}/triage_result", c.triageResult)
	mux.HandleFunc("/tests/upload_artifacts", c.uploadTestArtifact)
	mux.HandleFunc("/tests/upload", c.uploadTest)
	mux.HandleFunc("/trees", c.getTrees)
	mux.HandleFunc("/base_findings/upload", c.uploadBaseFinding)
	mux.HandleFunc("/base_findings/status", c.baseFindingStatus)
	return mux
}

func (c APIServer) getSessionSeries(w http.ResponseWriter, r *http.Request) {
	resp, err := c.seriesService.GetSessionSeries(r.Context(), r.PathValue("session_id"))
	if err == service.ErrSeriesNotFound || err == service.ErrSessionNotFound {
		http.Error(w, fmt.Sprint(err), http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON(w, resp)
}

func (c APIServer) triageResult(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.UploadTriageResultReq](w, r)
	if req == nil {
		return
	}
	err := c.sessionService.TriageResult(r.Context(), r.PathValue("session_id"), req)
	if errors.Is(err, service.ErrSessionNotFound) {
		http.Error(w, fmt.Sprint(err), http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[interface{}](w, nil)
}

func (c APIServer) getSeries(w http.ResponseWriter, r *http.Request) {
	resp, err := c.seriesService.GetSeries(r.Context(), r.PathValue("series_id"))
	if errors.Is(err, service.ErrSeriesNotFound) {
		http.Error(w, fmt.Sprint(err), http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON(w, resp)
}

func (c APIServer) uploadBuild(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.UploadBuildReq](w, r)
	if req == nil {
		return
	}
	resp, err := c.buildService.Upload(r.Context(), req)
	if err != nil {
		// TODO: sometimes it's not StatusInternalServerError.
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON(w, resp)
}

func (c APIServer) uploadTest(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.TestResult](w, r)
	if req == nil {
		return
	}
	// TODO: add parameters validation (and also of the Log size).
	err := c.testService.Save(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[interface{}](w, nil)
}

func (c APIServer) uploadTestArtifact(w http.ResponseWriter, r *http.Request) {
	const maxMemory = 16 * 1000 * 1000 // 16 MB.
	if err := r.ParseMultipartForm(maxMemory); err != nil {
		http.Error(w, "could not parse the multipart form", http.StatusBadRequest)
		return
	}
	defer r.MultipartForm.RemoveAll()

	file, _, err := r.FormFile("content")
	if err != nil {
		if err == http.ErrMissingFile {
			http.Error(w, "the 'content' file must be present", http.StatusBadRequest)
			return
		}
		http.Error(w, fmt.Sprintf("failed to query the file: %s", err), http.StatusInternalServerError)
		return
	}
	defer file.Close()

	err = c.testService.SaveArtifacts(r.Context(),
		r.FormValue("session"),
		r.FormValue("test"),
		file)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[interface{}](w, nil)
}

func (c APIServer) uploadFinding(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.NewFinding](w, r)
	if req == nil {
		return
	}
	// TODO: add parameters validation.
	err := c.findingService.Save(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[interface{}](w, nil)
}

func (c APIServer) getLastBuild(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.LastBuildReq](w, r)
	if req == nil {
		return
	}
	resp, err := c.buildService.LastBuild(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[*api.Build](w, resp)
}

func (c APIServer) uploadSeries(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.Series](w, r)
	if req == nil {
		return
	}
	resp, err := c.seriesService.UploadSeries(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[*api.UploadSeriesResp](w, resp)
}

func (c APIServer) uploadSession(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.NewSession](w, r)
	if req == nil {
		return
	}
	resp, err := c.sessionService.UploadSession(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[*api.UploadSessionResp](w, resp)
}

func (c APIServer) getTrees(w http.ResponseWriter, r *http.Request) {
	api.ReplyJSON(w, &api.TreesResp{
		Trees:       api.DefaultTrees,
		FuzzTargets: api.FuzzTargets,
	})
}

func (c APIServer) uploadBaseFinding(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.BaseFindingInfo](w, r)
	if req == nil {
		return
	}
	err := c.baseFindingService.Upload(r.Context(), req)
	if errors.Is(err, service.ErrBuildNotFound) {
		http.Error(w, fmt.Sprint(err), http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[interface{}](w, nil)
}

func (c APIServer) baseFindingStatus(w http.ResponseWriter, r *http.Request) {
	req := api.ParseJSON[api.BaseFindingInfo](w, r)
	if req == nil {
		return
	}
	resp, err := c.baseFindingService.Status(r.Context(), req)
	if err != nil {
		http.Error(w, fmt.Sprint(err), http.StatusInternalServerError)
		return
	}
	api.ReplyJSON[*api.BaseFindingStatus](w, resp)
}
